#include <vector>
#include <fstream>
#include <sstream>
#include <algorithm>
#include "icache/disc_cache.h"
#include "mswin/file_dir.h"
#include "imglib/io.h"
#include "imglib/image.h"
#include "throwWin32Error.h"

namespace icache {

string disc_cache::element::desc_rel_path() const {
	string res = rel_path;
	mswin::change_ext(res, "txt");
	return res;
}

disc_cache::disc_cache() : _file_release(false)
{
	erase_contents_on_exit = false;
	clear();
}

disc_cache::disc_cache(const string & root_path, disc_size_type quota, item_id_deserializer * d) : _file_release(false)
{
	erase_contents_on_exit = false;
	reinit(root_path, quota, d);
}

disc_cache::~disc_cache()
{
	if (erase_contents_on_exit)
		purge();
}

struct disc_cache::element_n_time : disc_cache::element {
	 FILETIME time;
	 //compare by time
	 friend bool operator < (element_n_time const & lhs, element_n_time const & rhs)
		{ return (ULONGLONG&)lhs.time < (ULONGLONG&)rhs.time; }
};

void disc_cache::reinit(const string & root_path, disc_size_type q, item_id_deserializer * d)
{
	if (erase_contents_on_exit)
		purge();

	mswin::shared_rw_resource::auto_wlock l(_entire_directory);

	_root_path = root_path;
	mswin::slash_ended(_root_path);
	mswin::createpath(_root_path);
	if (!_dir_lock.lock(_root_path.c_str())) {
		clear();
		return;
	}
	_dir_lock.unlock(); //to enumerate contents
	_quota = q;
	_size = 0;
	lru.clear();
	assert(items_in_use.empty());

    WIN32_FIND_DATA wfd;
    HANDLE          hFile;

	if ((hFile = FindFirstFile((_root_path + "*.png").c_str(), &wfd)) != INVALID_HANDLE_VALUE) 
	{
		std::vector<element_n_time> disc_contents;
		do {
			if (wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
				continue;
			disc_contents.push_back(element_n_time());
			element_n_time & e = disc_contents.back();
			e.rel_path = wfd.cFileName;

			string image_path = _root_path + e.rel_path;
			string description_path = _root_path + e.desc_rel_path();

			bool remove = false;
			if (d == 0)
				remove = true;
			else {
				std::ifstream description(description_path.c_str());
				if (!description)
					remove = true;
				else {
					e.id = d->parse(description);
					if (e.id.get() == 0)
						remove = true;
					else {
						if (wfd.nFileSizeHigh != 0)
							throw exception(std::string("File in cache is too big: ") + image_path);
						e.size = wfd.nFileSizeLow;
						e.time = wfd.ftLastWriteTime;
					}
				}
			}
			if (remove) {
				disc_contents.pop_back();
				if (mswin::remove(image_path))
					mswin::remove(description_path); ///< don't remove description if image is locked by other application
			}
		}
		while (FindNextFile(hFile, &wfd));

		if (!FindClose(hFile))
			throwWin32Error("FindClose");

		std::sort(disc_contents.begin(), disc_contents.end());
		lru.assign(disc_contents.begin(), disc_contents.end());
		std::vector<element_n_time>::iterator i;
		for (i = disc_contents.begin(); i != disc_contents.end(); ++i)
			_size += i->size;
		enforce_quota();
	}

	if (!_dir_lock.lock(_root_path.c_str())) {
		clear();
		return;
	}
}

void disc_cache::quota(disc_size_type q)
{
	if (q < _quota) {
		mswin::shared_rw_resource::auto_wlock l(_entire_directory);
		_quota = q;
		enforce_quota();
	}
	else
		_quota = q;
}

void disc_cache::purge()
{
	disc_size_type q = _quota;
	quota(0);
	quota(q);
}

void disc_cache::clear()
{
	_root_path.clear();
	_dir_lock.unlock();
	lru.clear();
	_size = 0;
	_quota = 0;
	assert (items_in_use.empty());
}

struct equal_item_id {
	item_id const & id;
	equal_item_id(item_id_sptr const & i) : id(*i) { }
	bool operator () (disc_cache::element const & e) const
		{ return e.id->equal(id); }
};

void disc_cache::enforce_quota()
{
	mswin::shared_resource::auto_lock l(_files);
	//leave on disc those recently files that fit in quota
	lru_type::iterator i = lru.begin();
	while (_size > _quota) {
		while (std::find_if(items_in_use.begin(), items_in_use.end(), equal_item_id(i->id)) != items_in_use.end()) {
			if (++i == lru.end()) {
				if (!items_in_use.empty()) {
					l.unlock();
					_file_release.wait();
					l.lock();
				}
				i = lru.begin();
			}
		}
		assert(i != lru.end());

		items_in_use.push_front(i->id);
		items_in_use_type::iterator j = items_in_use.begin();
		_size -= i->size;
		l.unlock();

		erase_element_on_disc(*i);

		l.lock();
		i = lru.erase(i);
		items_in_use.erase(j);
		_file_release.pulse();
	}
}

void disc_cache::erase_element_on_disc(element const & e)
{
	if (mswin::remove(_root_path + e.rel_path))
		mswin::remove(_root_path + e.desc_rel_path()); ///< don't remove description if image is locked by other application
}

disc_cache::claim_item::claim_item(disc_cache & dc, item_id_sptr const & id, disc_cache::claim_if_not_found policy) : c(dc)
{
	size_change = 0;
	lru_pos = dont_change_lru_position;

	mswin::shared_resource::auto_lock l(c._files);
	while (std::find_if(c.items_in_use.begin(), c.items_in_use.end(), equal_item_id(id)) != c.items_in_use.end()) {
		l.unlock();
		c._file_release.wait();
		l.lock();
	}

	_claimed = false;
	i = std::find_if(c.lru.begin(), c.lru.end(), equal_item_id(id));
	if (i == c.lru.end()) {
		if (policy == do_nothing)
			return;
		if (policy == create_new) {
			c.lru.push_front(disc_cache::element(id));
			i = c.lru.begin();
		}
	}

	_claimed = true;
	c.items_in_use.push_front(id);
	j = c.items_in_use.begin();
}

disc_cache::claim_item::~claim_item()
{
	if (!_claimed)
		return;
	mswin::shared_resource::auto_lock l(c._files);
	if (i != c.lru.end()) //exception for claim_but_not_create
		c.reposition_element(i, lru_pos);
	c._size += size_change;
	c.items_in_use.erase(j);
	c._file_release.pulse();
}

void disc_cache::reposition_element(lru_type::iterator i, last_used_position p)
{
	assert(i->size != 0);
	switch (p) {
	case least_recently_used:
		if (i != lru.begin())
			lru.splice(lru.begin(), lru, i, ++lru_type::iterator(i));
		break;
	case dont_change_lru_position:
		break;
	case most_recently_used:
		lru.splice(lru.end(), lru, i, ++lru_type::iterator(i));
		break;
	case erase_element_as_unused:
		lru.erase(i);
		break;
	default:
		assert(false);
	}
}

void disc_cache::write_element_on_disc(element & e, byte_image const & im)
{
	string name = e.id->name();
	for (int n = 0; ; ++n) {
		std::ostringstream s;
		s << name;
		if (n != 0)
			s << '.' << n;
		s << ".png";
		e.rel_path = s.str();
		if (!mswin::exists(_root_path + e.rel_path))
			break;
	}
	e.size = img::gdiplus_writer().write(_root_path + e.rel_path, "png", im);
	std::ofstream desc((_root_path + e.desc_rel_path()).c_str());
	e.id->serialize(desc);
	if (!desc)
		throw exception(std::string("file write error: ") + _root_path + e.desc_rel_path());
}

void disc_cache::read_element_from_disc(element const & e, byte_image & im)
{
	img::gdiplus_reader().read(_root_path + e.rel_path, "png", im);
}

void disc_cache::put(cached_image_sptr const & c, last_used_position p)
{
	const item_id_sptr & id = c->id; 
	const byte_image & im = c->im;
	if (p == least_recently_used) {
		size_t worst_size = 3*im.width()*im.height();
		if (_size + worst_size > _quota)
			return;
	}
	assert(p != dont_change_lru_position && p != erase_element_as_unused);
	mswin::shared_rw_resource::auto_rlock l(_entire_directory);
	if (!is_open())
		return;
	{
		claim_item cl(*this, id, create_new);
		assert(cl.claimed());
		cl.lru_pos = p;

		size_t old_size = cl.e().size;
		try {
			write_element_on_disc(cl.e(), im);
		}
		catch (...) {
			cl.lru_pos = erase_element_as_unused;
			throw;
		}
		cl.size_change = cl.e().size - old_size;
	}

	enforce_quota();
}

cached_image_sptr disc_cache::get(item_id_sptr const & id, last_used_position p)
{
	assert(id->loadable());
	assert(p != dont_change_lru_position && p != erase_element_as_unused);
	mswin::shared_rw_resource::auto_rlock l(_entire_directory);
	cached_image_sptr c(new cached_image(id));
	if (!is_open()) {
		id->load(c->im);
		return c;
	}
	{
		claim_item cl(*this, id, create_new);
		assert(cl.claimed());
		cl.lru_pos = p;

		try {
			if (cl.e().size != 0) {
				///it's not new element to cache
				read_element_from_disc(cl.e(), c->im);
			}
			else {
				///new element
				id->load(c->im);
				write_element_on_disc(cl.e(), c->im);
				cl.size_change = cl.e().size;
			}
		}
		catch (...) {
			cl.lru_pos = erase_element_as_unused;
			throw;
		}
	}

	enforce_quota();
	return c;
}

cached_image_sptr disc_cache::try_get(item_id_sptr const & id, last_used_position p)
{
	mswin::shared_rw_resource::auto_rlock l(_entire_directory);
	if (!is_open()) 
		return cached_image_sptr();

	claim_item cl(*this, id, do_nothing);
	if (!cl.claimed())
		return cached_image_sptr();
	cl.lru_pos = p;
	assert(cl.e().size != 0);
	cached_image_sptr c(new cached_image(id));

	try {
		read_element_from_disc(cl.e(), c->im);
	}
	catch (...) {
		cl.lru_pos = erase_element_as_unused;
		throw;
	}
	return c;
}

bool disc_cache::touch(item_id_sptr const & id, last_used_position p)
{
	mswin::shared_rw_resource::auto_rlock l(_entire_directory);
	if (!is_open()) 
		return false;
	mswin::shared_resource::auto_lock l2(_files);
	if (std::find_if(items_in_use.begin(), items_in_use.end(), equal_item_id(id)) != items_in_use.end())
		return true;

	lru_type::iterator i = std::find_if(lru.begin(), lru.end(), equal_item_id(id));
	if (i == lru.end())
		return false;

	reposition_element(i, p);
	return true;
}

void disc_cache::remove(item_id_sptr const & id)
{
	mswin::shared_rw_resource::auto_rlock l(_entire_directory);
	if (!is_open()) 
		return;
	claim_item cl(*this, id, do_nothing);
	if (!cl.claimed())
		return;
	cl.lru_pos = erase_element_as_unused;
	cl.size_change = -(int)cl.e().size;

	erase_element_on_disc(cl.e());
}

int disc_cache::items()
{
	mswin::shared_rw_resource::auto_rlock l(_entire_directory);
	mswin::shared_resource::auto_lock l2(_files);
	return lru.size();
}

} //namespace icache
